﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Model;
using Kaleida.ServiceMonitor.Model.Layout;
using Kaleida.ServiceMonitor.Model.Parsing;
using Kaleida.ServiceMonitor.Model.Runtime;
using log4net;
using System.Linq;
using Monitor = Kaleida.ServiceMonitor.Model.Monitor;

namespace Kaleida.ServiceMonitor.UI
{
    public partial class MainForm : Form
    {
        private bool isFullScreen;
        private readonly IScriptStateLayoutStrategy layout = new NestedTilesLayoutStrategy();
        private readonly ServiceMonApplication application;

        private readonly string initialScriptNameOrPath;
        
        private bool autoStart;
        
        private bool windowIsActive = true;

        private static readonly ILog log = LogManager.GetLogger(typeof (MainForm));
        private readonly NotifyIcon trayIcon;
        private readonly ContextMenu trayMenu;
        private bool updatingUI;
    	private AboutControl aboutControl;
        readonly ISet<INotificationPanel> naughtyPanelList = new HashSet<INotificationPanel>();
        private readonly NotificationPanelFactory panelFactory;
		private IList<NotificationPanelTabPage> notificationPanelTabPages;
        private readonly IList<IModuleNotificationPanel> moduleNotificationPanels = new List<IModuleNotificationPanel>();
        private readonly IList<IApplicationNotificationPanel> applicationNotificationPanels = new List<IApplicationNotificationPanel>();
        private readonly SplashScreen splashScreen = new SplashScreen();
        
        public MainForm([CanBeNull]string scriptNameOrPath = null)
        {
            ShowSplashScreen();

            var configuration = new FileBasedConfiguration();
            application = new ServiceMonApplication(configuration);
            initialScriptNameOrPath = scriptNameOrPath ?? configuration.InitialScriptName ?? "";

            Application.ThreadException += UnhandledExceptionHandler;

            TryInitialiseFromConfig();

            trayMenu = new ContextMenu();
            trayMenu.MenuItems.Add("Exit", TrayIconMenuExitClicked);

            trayIcon = new NotifyIcon {ContextMenu = trayMenu, Visible = true};
            trayIcon.MouseUp += OnTrayIconClick;

            InitializeComponent();
            
            lblScriptSummary.Text = "";

            workspaceControl.Workspace = application.Workspace;
            workspaceControl.ScriptDirectoryPath = application.Configuration.ScriptsPath;

            application.ScriptCompilationComplete += ScriptCompilationComplete;
            application.Workspace.CurrentScriptChanged += SelectedScriptChanged;

            panelFactory = new NotificationPanelFactory(new[] { Assembly.GetExecutingAssembly() });
            
            AddNotificationTabPanels();
            AddHelpTabPage();
        }

        private void ShowSplashScreen()
        {
            splashScreen.Show();
            splashScreen.Update();
        }

        private void HideSplashScreen()
        {
            splashScreen.Hide();
        }

        private void AddNotificationTabPanels()
        {
            var modulePanelTypes = panelFactory.GetPanelTypes(typeof (IModuleNotificationPanel)).OrderBy(i=>i.GetPanelName());
            var applicationPanelTypes = panelFactory.GetPanelTypes(typeof (IApplicationNotificationPanel)).OrderBy(i=>i.GetPanelName());

            var allPanelTypes = modulePanelTypes.Concat(applicationPanelTypes);
            notificationPanelTabPages = allPanelTypes.Select(i => new NotificationPanelTabPage(i)).ToList();

			tabControl.TabPages.AddRange(notificationPanelTabPages.Cast<TabPage>().ToArray());

            applicationNotificationPanels.Clear();
            foreach(var panelTab in notificationPanelTabPages)
            {
                if(panelTab.Type.DerivesFrom(typeof(IApplicationNotificationPanel)))
                {
                    var panel = panelFactory.CreatePanel<IApplicationNotificationPanel>(panelTab.Type);
                    panel.Initialise(application);
                    panelTab.SetPanel(panel);

                    applicationNotificationPanels.Add(panel);
                }
            }
        }

        private void AddHelpTabPage()
        {
            aboutControl = new AboutControl {Dock = DockStyle.Fill};
            var tabPage = new TabPage("Help");
            tabPage.Controls.Add(aboutControl);
            tabControl.TabPages.Add(tabPage);
        }

        public void TryInitialiseFromConfig()
        {
            try
            {
                autoStart = application.Configuration.AutoStart;
            }
            catch (Exception exception)
            {
                MessageBox.Show(string.Format("Error Reading config: {0}", exception));
            }
        }

        private void UnhandledExceptionHandler(object sender, ThreadExceptionEventArgs e)
        {
            log.Fatal(e.Exception);
            MessageBox.Show(string.Format("ERROR: {0}\r\nSee log files for more information", e.Exception.Message), "An unexpected error occured", MessageBoxButtons.OK, MessageBoxIcon.Error);
            Application.Exit();
        }

        protected override void OnLoad(EventArgs e)
        {
            ScriptParser.Initialise(application.Configuration.OperationsPath);
            
            aboutControl.PopulateOperationsHelp(ScriptParser.GetRuntimeEnvironment());
            
            UpdateWindowTitle();

            Icon = ApplicationIcon.Application;

            components = new Container();
            components.Add(trayIcon);

            application.Workspace.LoadAllScriptsFromDirectory(application.Configuration.ScriptsPath);
            TryLoadSpecificScriptFile(initialScriptNameOrPath);
            
            application.SendMessageToAllMonitors(i=>i.ClearFailures());

            if (autoStart && application.HasSuccessfullyCompiledScript)
            {
                StartMonitoring();
            }
            else
            {
                StopMonitoring();
            }

            ThreadPool.QueueUserWorkItem(i => CheckForNewVersion());

            base.OnLoad(e);

            HideSplashScreen();
        }

        private void TryLoadSpecificScriptFile(string scriptPath)
        {
            if (scriptPath != "" && File.Exists(scriptPath))
            {
                try
                {
                    application.Workspace.LoadScript(scriptPath);
                }
                catch (FileNotFoundException)
                {
                }
            }
            else
            {
                workspaceControl.TrySelectScript(initialScriptNameOrPath);
            }
        }

        private void CheckForNewVersion()
        {
            var versionCheck = new WebVersionCheck();
            var result = versionCheck.CheckLatestVersion();

            BeginInvoke(new EventHandler(delegate
                                             {
                                                 if (!result.Success)
                                                 {
                                                     aboutControl.SetVersionCheckStatus("Could not check for new version", result.Exception.Message);
                                                 }
                                                 else
                                                 {
                                                     if (ServiceMonApplication.VersionNumber == result.LatestVersionNumber)
                                                     {
                                                         aboutControl.SetVersionCheckStatus("This is the latest stable version");
                                                     }
                                                     else if (ServiceMonApplication.VersionNumber > result.LatestVersionNumber)
                                                     {
                                                         aboutControl.SetVersionCheckStatus("This is a pre-release version. The latest stable version is v" + result.LatestVersionNumber);
                                                     }
                                                     else
                                                     {
                                                         var linkText = string.Format("Version '{0}' is available. Click to download", result.LatestVersionNumber);
                                                         
                                                         newVersionLink.Text = linkText;
                                                         newVersionLink.Tag = result.LatestVersionUrl;
                                                         newVersionLink.Visible = true;
                                                     }
                                                 }
                                             }));
        }

        private void UpdateWindowTitle()
        {
            var title = new StringBuilder();

            title.AppendFormat("{0} - ", application.Workspace.CurrentScript.Name);

            title.AppendFormat("ServiceMon v{0}", ServiceMonApplication.VersionNumber);
            Text = title.ToString();
        }

        private void SelectedScriptChanged(object sender, EventArgs e)
        {
            SendMessageToNotificationPanels(i => i.RefreshContent());

            UpdateWindowTitle();

            tabControl.SelectTab(tabPageScript);
        }

        private void SendMessageToNotificationPanels(Action<INotificationPanel> message)
        {
            var notificationPanels = moduleNotificationPanels.Concat(applicationNotificationPanels.Cast<INotificationPanel>());
            foreach (var notificationPanel in notificationPanels)
            {
                if (naughtyPanelList.Contains(notificationPanel)) continue;

                try
                {
                    message(notificationPanel);
                }
                catch (Exception exception)
                {
                    naughtyPanelList.Add(notificationPanel);

                    log.Error(string.Format("Panel '{0}' threw an unhandled exception: {1}", notificationPanel.PanelName, exception));
                    notificationPanel.GetPanelControl().Controls.Clear();

                    var errorMessage = new StringBuilder();
                    errorMessage.AppendFormat("An unhandled Exception occured: {0}\r\n\r\n", exception.Message);
                    errorMessage.AppendFormat("This panel will not be sent new messages until ServiceMon is restarted\r\n");
                    errorMessage.AppendFormat("Please see logs for more information");

                    var errorLabel = new Label
                                         {
                                             AutoSize = true,
                                             ForeColor = Color.Red,
                                             Text = errorMessage.ToString()
                                         };

                    notificationPanel.GetPanelControl().Controls.Add(errorLabel);
                }
            }
        }

        private void ScriptCompilationComplete(object sender, ScriptCompilationCompleteEventArgs args)
        {
            var compilation = args.ScriptCompilation;

            lock (this)
            {
                if (compilation.Succeeded)
                {
                	AddAreasToModuleNotificationTabs(compilation.CompiledScript.Root);

                	var singleModule = application.Monitors.Count() == 1;

                	pollIntervalControl.Visible = singleModule;
                	lblPollInterval.Visible = singleModule;

                	btnStart.Text = singleModule ? "Start" : "Start All";
					btnStop.Text = singleModule ? "Stop" : "Stop All";

                    application.SendMessageToAllMonitors(monitor =>
                                                             {

                                                                 monitor.Statistics.Initialise(monitor.CompiledModule.Operations);
																 pollIntervalControl.Value = monitor.CompiledModule.PollInterval;
                                                                 monitor.SendMessageToExtensions(i => i.ScriptCompilationComplete(args));
                                                             });
                }
            }
        }

    	private void AddAreasToModuleNotificationTabs(CompiledNode rootScriptNode)
    	{
    		moduleNotificationPanels.Clear();
			SuspendLayout();
    		foreach (var tab in notificationPanelTabPages.Where(i=>i.Type.DerivesFrom(typeof(IModuleNotificationPanel))))
    		{
				tab.Controls.Clear();
				var scriptNodeAreas = layout.CalculateLayout(rootScriptNode, tab.ClientRectangle);
				foreach (var scriptNodeArea in scriptNodeAreas)
				{
					var node = scriptNodeArea.ScriptNode as CompiledNode;

					if (node != null && node.IsLeaf)
					{
						var panelControl = BuildPanelControl(tab.Type, node.ScriptModule, scriptNodeArea);
						tab.Controls.Add(panelControl);
					}

				}
    		}
			ResumeLayout();
    	}

    	private ModulePanelControl BuildPanelControl(Type panelType, CompiledScriptModule scriptModule, ScriptNodeArea scriptNodeArea)
    	{
    		var monitor = application.TryGetMonitor(scriptModule);

    		Control contentControl;
			if (monitor == null)
			{
				contentControl = new Panel { BackColor = Color.Red };
				contentControl.Controls.Add(new Label
				                          	{
				                          		Text = "Error: Could not find panel's Monitor instance",
												Dock = DockStyle.Fill
				                          	});
			}
			else
			{
				var panel = panelFactory.CreatePanel<IModuleNotificationPanel>(panelType);
				panel.Initialise(monitor);
				moduleNotificationPanels.Add(panel);

				contentControl = panel.GetPanelControl();
			}

    	    var panelControl = new ModulePanelControl(scriptModule, contentControl);
            contentControl.Location = scriptNodeArea.Area.Location;
            contentControl.Size = scriptNodeArea.Area.Size;
    	    return panelControl;
    	}
        
        private void OnTrayIconClick(object o, EventArgs e)
        {
            ShowInTaskbar = true;
            Visible = true;
            WindowState = FormWindowState.Normal;
        }

        protected override void OnResize(EventArgs e)
        {
            if (WindowState == FormWindowState.Minimized)
            {
                ShowInTaskbar = false;
                Visible = false;
            }
            else
            {
                base.OnResize(e);
            }
        }

        private void TrayIconMenuExitClicked(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void StartMonitoring()
        {
            application.SendMessageToAllMonitors(i =>
                                                     {
                                                         if (!i.State.CanBeginMonitoring)
                                                             return;

                                                         i.State.StartMonitoring();

                                                         if (tabControl.SelectedTab == tabPageScript)
                                                         {
                                                             TrySelectFirstNotificationPanel();
                                                         }

                                                         ThreadPool.QueueUserWorkItem(j => i.InvokeNextOperation());
                                                     });
        }

        private void TrySelectFirstNotificationPanel()
        {
            var firstPanel = tabControl.TabPages.Cast<TabPage>().FirstOrDefault(i => i is NotificationPanelTabPage);
            if (firstPanel != null)
            {
                tabControl.SelectTab(firstPanel);
            }
        }

        private void StopMonitoring()
        {
            application.SendMessageToAllMonitors(monitor =>
                                                     {
                                                         var state = monitor.State;
                                                         if (!state.IsMonitoring)
                                                             return;

                                                         state.StopMonitoring();

                                                         var compilation = application.CurrentCompilation;
                                                         if (compilation != null && compilation.Succeeded && compilation.CompiledScript.Root.IsLeaf)
                                                         {
                                                             pollIntervalControl.Value = compilation.CompiledScript.Root.ScriptModule.PollInterval;
                                                         }
                                                     });
        }

        private string GetSystemTrayTitle(string message)
        {
            var title = new StringBuilder();
            title.AppendFormat("{0}: ", application.ScriptName);
            title.Append(message);

            return title.ToString();
        }

        private void SetSystemTray(string message, Icon icon)
        {
            var text = GetSystemTrayTitle(message);

            var shortText = text.Abbreviate(60);

            text = shortText;

            trayIcon.Icon = new Icon(icon, 16, 16);
            trayIcon.Text = text;
        }

        private void UpdateCurrentScriptSummary()
        {
            lblScriptSummary.ResetBackColor();
            var compilation = application.CurrentCompilation;
            if (compilation == null)
            {
                lblScriptSummary.ForeColor = Color.Black;
                lblScriptSummary.Text = "";
            }
            else if (compilation.Succeeded)
            {
                lblScriptSummary.ForeColor = Color.Black;
                lblScriptSummary.Text = compilation.CompiledScript.BuildSummary();                
            }
            else
            {
                var message = "Cannot Parse Script: " + compilation.Exception.Message;
                SetSystemTray(message, ApplicationIcon.Error);

                lblScriptSummary.ForeColor = Color.Red;
                lblScriptSummary.Text = message;
                lblScriptSummary.BackColor = Color.FromArgb(255, 239, 239);
            }
        }

        private void UpdateUIFromModel()
        {
        	var monitors = application.Monitors;
        	bool anyBadExtensions = monitors.Any(i => i.HasMisbehavingExtensions);
            var pluginIssues = naughtyPanelList.Any() || anyBadExtensions;

            lblPluginErrorSummary.Text = pluginIssues ? "Error with plugin. Please see log." : "";
            lblPluginErrorSummary.Visible = pluginIssues;

        	bool startEnabled = monitors.Any(i => i.State.CanBeginMonitoring);
        	bool stopEnabled = monitors.Any(i => i.State.IsMonitoring);
        	bool freqEnabled = monitors.Count() == 1 && monitors.Any(i => i.State.IsMonitoring);
			bool ackEnabled = monitors.Any(i => i.State.HasFailures);

			btnStart.Enabled = startEnabled;
			btnStop.Enabled = stopEnabled;

			pollIntervalControl.Enabled = freqEnabled;

			btnClearFailures.Enabled = ackEnabled;

            var focusedControl = FindFocusedControl(this);
            SendMessageToNotificationPanels(i => i.RefreshContent());

            if (windowIsActive && focusedControl != null)
            {
                focusedControl.Focus();
            }

            Icon systemTrayIcon = GetSystemTrayIcon();

            string statusSummary = BuildStatusSummary(monitors);
            SetSystemTray(statusSummary, systemTrayIcon);

			string statusBarMessage = statusSummary + (monitors.Any(i => i.State.IsMonitoring) ? " " + GetSpinner() : "");
			tsStatusLabel.SetMessage(statusBarMessage, application.HasScriptError || monitors.Any(i => i.State.HasFailures) ? Color.Red : monitors.Any(i => i.State.IsMonitoring) ? Color.Green : Color.Black);

            UpdateCurrentScriptSummary();
            
        }

    	private string BuildStatusSummary(IList<Monitor> monitors)
    	{
    		if (monitors.Count() == 1)
				return monitors.Single().State.DefaultPresentation.GetStateSummary();

    		var totalCount = monitors.Count(i => i.State.IsMonitoring);
			var errorCount = monitors.Count(i => i.State.IsMonitoring && i.State.HasFailures);

    		return errorCount == 0
    		       	? totalCount + " module(s) monitoring without error."
    		       	: errorCount + " module(s) out of " + totalCount + " have reported errors.";
    	}

    	[CanBeNull]
        public static Control FindFocusedControl(Control control)
        {
            var container = control as ContainerControl;
            while (container != null)
            {
                control = container.ActiveControl;
                container = control as ContainerControl;
            }
            return control;
        }

        private Icon GetSystemTrayIcon()
        {
            bool hasErrors = application.HasScriptError || application.Monitors.Any(i => i.State.HasFailures);

            if (hasErrors)
                return ApplicationIcon.Error;

            bool anyRunning = application.Monitors.Any(i => i.State.IsMonitoring);
            return anyRunning ? ApplicationIcon.Running : ApplicationIcon.Stopped;
        }

        private string GetSpinner()
        {
            const int width = 60;
            var position = DateTime.Now.Second % width;

            return new string('.', position) + new string(' ', width - position);
        }

        private void StartButtonClick(object sender, EventArgs e)
        {
            StartMonitoring();
        }

        private void StopButtonClick(object sender, EventArgs e)
        {
            StopMonitoring();
        }

        private void UIUpdateTimerTick(object sender, EventArgs e)
        {
            if (updatingUI) return;

            updatingUI = true;
            UpdateUIFromModel();
            updatingUI = false;
        }

        private void pollIntervalControl_ValueChanging(object sender, PollIntervalValueChangingEventArgs e)
        {
            e.NewValue = PollIntervalCriteria.EnsureMet(e.NewValue);
        }

        private void pollIntervalControl_ValueChanged(object sender, EventArgs e)
        {
            var monitors = application.Monitors;
            if(monitors.Count() == 1 && monitors.Any(i => i.State.IsMonitoring))
            {
                var duration = pollIntervalControl.Value;
                monitors.Single().CompiledModule.PollInterval = duration;                
            }
        }

        private void ClearFailuresClick(object sender, EventArgs e)
        {
            application.SendMessageToAllMonitors(i => i.ClearFailures());
        }

        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = workspaceControl.AllowUserToSaveDirty();
        }
        
        private static void BrowseTo(string url)
        {
            var sInfo = new ProcessStartInfo(url);
            Process.Start(sInfo);
        }

        private void newVersionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            var url = (string) newVersionLink.Tag;

            if (!string.IsNullOrEmpty(url))
            {
                BrowseTo(url);
            }
        }

        private void MainForm_Enter(object sender, EventArgs e)
        {
            windowIsActive = true;
        }

        private void MainForm_Leave(object sender, EventArgs e)
        {
            windowIsActive = false;
        }

        private void MainForm_Resize(object sender, EventArgs e)
        {
            UpdateLayout();
        }

        private void UpdateLayout()
        {
            SuspendLayout();
            var isMini = Height < 300 || Width < 580;

            ReLayoutNotificationPanels();

            statusStrip.Visible = !isMini && !isFullScreen;
            pnlTopBar.Visible = !isMini && !isFullScreen;
            newVersionLink.Visible = newVersionLink.Visible && !isMini;
            newVersionLink.Visible = newVersionLink.Visible && !isMini && !isFullScreen;

            if (isMini || isFullScreen)
            {
                tabControl.Dock = DockStyle.Fill;
                tabControl.BringToFront();
            }
            else
            {
                tabControl.Dock = DockStyle.None;
                tabControl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
                tabControl.Location = new Point(0, 36);
                tabControl.Size = new Size(Width - 16, Height - 100);
            }

            splitContainer.Panel2MinSize = Height < 350 ? 0 : 50;

            FormBorderStyle = isFullScreen ? FormBorderStyle.None : FormBorderStyle.Sizable;
            ResumeLayout();
        }

        private void ReLayoutNotificationPanels()
    	{
    		var compilation = application.CurrentCompilation;
			if(compilation == null || !compilation.Succeeded)
				return;

			foreach (var tab in notificationPanelTabPages.Where(i=>i.Type.DerivesFrom(typeof(IModuleNotificationPanel))))
			{
				var scriptNodeAreas = layout.CalculateLayout(compilation.CompiledScript.Root, tab.ClientRectangle);
                foreach (ModulePanelControl control in tab.Controls)
				{
					var module = control.Module;
					var scriptNodeArea = scriptNodeAreas.FirstOrDefault(i => i.ScriptNode is CompiledNode && ((CompiledNode) i.ScriptNode).ScriptModule == module);

					if(scriptNodeArea != null)
					{
						control.Location = scriptNodeArea.Area.Location;
						control.Size = scriptNodeArea.Area.Size;
					}
				}
			}
    	}

		private void tabControl_SelectedIndexChanged(object sender, EventArgs e)
		{
            SuspendLayout();

            foreach(var tab in notificationPanelTabPages)
            {
                foreach (ModulePanelControl control in tab.Controls)
                {
                    control.ZoomNormal();
                }
            }

			ReLayoutNotificationPanels();
            ResumeLayout();
		}

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if (keyData == Keys.F11)
            {
                isFullScreen = !isFullScreen;

                if(isFullScreen)
                    WindowState = FormWindowState.Maximized;

                UpdateLayout();
                return true;
            }
            return base.ProcessCmdKey(ref msg, keyData);
        }
    }
}
